В исследовании участвует стартап, который продает продукты питания. Нам нужно разобраться как ведут себя пользователи нашего мобильного приложения. Изучить варонки продаж. Провести исследования влияния изменения шрифта на поведение пользователей за время проведения A/A/B-эксперимента.
import pandas as pd
import numpy as np
import math as mth
import seaborn as sns
sns.set(rc={'figure.figsize':(16, 9)})
import matplotlib.pyplot as plt
from scipy import stats as st
import plotly.express as px
from plotly import graph_objects as go
import math
from scipy. special import logsumexp
# загружаем данные
try:
data = pd.read_csv(r"B:\Downloads\logs_exp.csv", sep = '\t')
except:
data = pd.read_csv("/datasets/logs_exp.csv", sep = '\t')
Описание данных
Согласно документации к данным:
Каждая запись в логе — это действие пользователя, или событие.
# выводим информацию о данных
display(data.head())
display(data.info())
display(data.describe())
print('Количество дубликатов:', data.duplicated().sum())
| EventName | DeviceIDHash | EventTimestamp | ExpId | |
|---|---|---|---|---|
| 0 | MainScreenAppear | 4575588528974610257 | 1564029816 | 246 |
| 1 | MainScreenAppear | 7416695313311560658 | 1564053102 | 246 |
| 2 | PaymentScreenSuccessful | 3518123091307005509 | 1564054127 | 248 |
| 3 | CartScreenAppear | 3518123091307005509 | 1564054127 | 248 |
| 4 | PaymentScreenSuccessful | 6217807653094995999 | 1564055322 | 248 |
<class 'pandas.core.frame.DataFrame'> RangeIndex: 244126 entries, 0 to 244125 Data columns (total 4 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 EventName 244126 non-null object 1 DeviceIDHash 244126 non-null int64 2 EventTimestamp 244126 non-null int64 3 ExpId 244126 non-null int64 dtypes: int64(3), object(1) memory usage: 7.5+ MB
None
| DeviceIDHash | EventTimestamp | ExpId | |
|---|---|---|---|
| count | 2.441260e+05 | 2.441260e+05 | 244126.000000 |
| mean | 4.627568e+18 | 1.564914e+09 | 247.022296 |
| std | 2.642425e+18 | 1.771343e+05 | 0.824434 |
| min | 6.888747e+15 | 1.564030e+09 | 246.000000 |
| 25% | 2.372212e+18 | 1.564757e+09 | 246.000000 |
| 50% | 4.623192e+18 | 1.564919e+09 | 247.000000 |
| 75% | 6.932517e+18 | 1.565075e+09 | 248.000000 |
| max | 9.222603e+18 | 1.565213e+09 | 248.000000 |
Количество дубликатов: 413
Файл содержит информацию о 244126 событиях.
Время события сохранено в секундах, заголовки столбцов не совсем удобны, заметное количество дубликатов. Исправим это.
# меняем названия столбцов
data.columns = ['event', 'user_id', 'event_time', 'group']
# приводим дату в формат to_datetime и создаем новый столбец date
data['event_time'] = pd.to_datetime(data['event_time'], unit = 's')
# создаем новый столбец date
data['date'] = pd.to_datetime(data['event_time'].dt.date)
data
| event | user_id | event_time | group | date | |
|---|---|---|---|---|---|
| 0 | MainScreenAppear | 4575588528974610257 | 2019-07-25 04:43:36 | 246 | 2019-07-25 |
| 1 | MainScreenAppear | 7416695313311560658 | 2019-07-25 11:11:42 | 246 | 2019-07-25 |
| 2 | PaymentScreenSuccessful | 3518123091307005509 | 2019-07-25 11:28:47 | 248 | 2019-07-25 |
| 3 | CartScreenAppear | 3518123091307005509 | 2019-07-25 11:28:47 | 248 | 2019-07-25 |
| 4 | PaymentScreenSuccessful | 6217807653094995999 | 2019-07-25 11:48:42 | 248 | 2019-07-25 |
| ... | ... | ... | ... | ... | ... |
| 244121 | MainScreenAppear | 4599628364049201812 | 2019-08-07 21:12:25 | 247 | 2019-08-07 |
| 244122 | MainScreenAppear | 5849806612437486590 | 2019-08-07 21:13:59 | 246 | 2019-08-07 |
| 244123 | MainScreenAppear | 5746969938801999050 | 2019-08-07 21:14:43 | 246 | 2019-08-07 |
| 244124 | MainScreenAppear | 5746969938801999050 | 2019-08-07 21:14:58 | 246 | 2019-08-07 |
| 244125 | OffersScreenAppear | 5746969938801999050 | 2019-08-07 21:15:17 | 246 | 2019-08-07 |
244126 rows × 5 columns
# удаляем дубликаты
data = data.drop_duplicates().reset_index(drop=True)
print('Количество дубликатов:', data.duplicated().sum())
Количество дубликатов: 0
# выводим информацию о данных после обработки
display(data.info())
data.head(5)
<class 'pandas.core.frame.DataFrame'> RangeIndex: 243713 entries, 0 to 243712 Data columns (total 5 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 event 243713 non-null object 1 user_id 243713 non-null int64 2 event_time 243713 non-null datetime64[ns] 3 group 243713 non-null int64 4 date 243713 non-null datetime64[ns] dtypes: datetime64[ns](2), int64(2), object(1) memory usage: 9.3+ MB
None
| event | user_id | event_time | group | date | |
|---|---|---|---|---|---|
| 0 | MainScreenAppear | 4575588528974610257 | 2019-07-25 04:43:36 | 246 | 2019-07-25 |
| 1 | MainScreenAppear | 7416695313311560658 | 2019-07-25 11:11:42 | 246 | 2019-07-25 |
| 2 | PaymentScreenSuccessful | 3518123091307005509 | 2019-07-25 11:28:47 | 248 | 2019-07-25 |
| 3 | CartScreenAppear | 3518123091307005509 | 2019-07-25 11:28:47 | 248 | 2019-07-25 |
| 4 | PaymentScreenSuccessful | 6217807653094995999 | 2019-07-25 11:48:42 | 248 | 2019-07-25 |
Изменили названия столбцов. Время событий в секундах привели в формат to_datetime и создали новый столбец date.Удалили дубликаты.
Распределение событий по пользователям
print(f'Всего в логе осталось {len(data)} событий.')
print(f'Всего пользователей в логе {len(data.user_id.unique())}.')
print(f'В среднем на пользователя приходится {int(len(data) / len(data.user_id.unique()))} события.')
data.groupby('user_id')[['event']].count().describe(percentiles=[0.05, 1/4, 1/2, 3/4, 0.95, 0.99])
Всего в логе осталось 243713 событий. Всего пользователей в логе 7551. В среднем на пользователя приходится 32 события.
| event | |
|---|---|
| count | 7551.000000 |
| mean | 32.275593 |
| std | 65.154219 |
| min | 1.000000 |
| 5% | 3.000000 |
| 25% | 9.000000 |
| 50% | 20.000000 |
| 75% | 37.000000 |
| 95% | 89.000000 |
| 99% | 200.500000 |
| max | 2307.000000 |
В среднем, на каждого пользователя приходится 32 события. Медианное количество при этом составляет 20, и всего у одного процента пользователей количество событий переваливает за 200.
plt.figure(figsize=(15, 7))
sns.histplot(data=data.groupby('user_id')[['event']].count(), x='event', kde=True)
plt.title('Распределение событий по пользователям')
plt.xlabel('Количество событий')
plt.ylabel('Количество пользователей')
plt.xlim(0,201)
plt.show()
Оценим, 32 события на пользователя - много ли это? Кажется, что не очень. Посмотрим, по сколько раз пользователи заходят в приложение - соберем события по людям и датам, считая, что за день - только одна сессия.
users_dates = data.groupby(['user_id', 'date'])['event'].count().reset_index()
# количество событий за сессию
users_dates.event.describe(percentiles=[0.05, 1/4, 1/2, 3/4, 0.95, 0.99])
count 27226.000000 mean 8.951480 std 22.977802 min 1.000000 5% 1.000000 25% 3.000000 50% 5.000000 75% 10.000000 95% 24.000000 99% 51.000000 max 2190.000000 Name: event, dtype: float64
Получается, в среднем на сессию приходится 5 событий. При четырех основных этапов - главная, каталог, корзина, оплата - это логично, например, после оплаты вернуться на главную автоматически. Некоторые листают приложение дольше.
Посмотрим, по сколько сессий приходится на человека
print(users_dates.groupby('user_id')['date'].count().describe(percentiles=[0.05, 1/4, 1/2, 3/4, 0.95, 0.99]))
count 7551.000000 mean 3.605615 std 1.951651 min 1.000000 5% 1.000000 25% 2.000000 50% 3.000000 75% 5.000000 95% 7.000000 99% 8.000000 max 9.000000 Name: date, dtype: float64
plt.figure(figsize=(10, 5))
sns.histplot(data=users_dates.groupby('user_id')[['date']].count(), x='date', bins=8)
plt.title('Распределение визитов по пользователям')
plt.xlabel('Количество дней-визитов')
plt.ylabel('Количество пользователей')
plt.show()
Медианное количество пользователей 3 за две недели зашел в приложение и нажал на 5 страничек.
Распределение логов по времени
# находим максимальную дату в логе
print(data.event_time.max())
2019-08-07 21:15:17
# находим минимальную дату в логе
print(data.event_time.min())
2019-07-25 04:43:36
# считаем количество event по date
data.groupby(by='date').agg({'event': 'count'}).sort_values(by='event', ascending=False).reset_index()
| date | event | |
|---|---|---|
| 0 | 2019-08-01 | 36141 |
| 1 | 2019-08-05 | 36058 |
| 2 | 2019-08-06 | 35788 |
| 3 | 2019-08-02 | 35554 |
| 4 | 2019-08-03 | 33282 |
| 5 | 2019-08-04 | 32968 |
| 6 | 2019-08-07 | 31096 |
| 7 | 2019-07-31 | 2030 |
| 8 | 2019-07-30 | 412 |
| 9 | 2019-07-29 | 184 |
| 10 | 2019-07-28 | 105 |
| 11 | 2019-07-27 | 55 |
| 12 | 2019-07-26 | 31 |
| 13 | 2019-07-25 | 9 |
# строим гистограмму по дате и времени
plt.figure(figsize=(15, 7))
data['date'].hist(bins=30)
plt.title('Гистограмма по дате и времени')
plt.xlabel("Дата")
plt.ylabel("Частота")
plt.xticks(rotation=45)
plt.show()
# код ревьюера
data['event_time'].hist(bins=14*24, figsize=(14, 5));
В имеющихся данных информация за две недели - с 25 июля по 7 августа 2019 года. 1 августа происходит резкий скачок количества событий - с двух тысяч до 36. Скорее всего, во вторую неделю проходила рекламная кампания, которая так повлияла на данные. Следует считать данные первой недели устаревшими. Отфильтруем их.
# отфильтровываем данные по дате
data_2w = data.query('date >= datetime(2019, 8, 1).date()')
print(data_2w.info())
print()
print(f'{(len(data_2w) / len(data)):.2%} - доля событий второй недели')
print(f'Всего пользователей во второй неделе {len(data_2w.user_id.unique())}.')
print()
print(data_2w.groupby('group')['user_id'].nunique())
<class 'pandas.core.frame.DataFrame'> Int64Index: 240887 entries, 2826 to 243712 Data columns (total 5 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 event 240887 non-null object 1 user_id 240887 non-null int64 2 event_time 240887 non-null datetime64[ns] 3 group 240887 non-null int64 4 date 240887 non-null datetime64[ns] dtypes: datetime64[ns](2), int64(2), object(1) memory usage: 11.0+ MB None 98.84% - доля событий второй недели Всего пользователей во второй неделе 7534. group 246 2484 247 2513 248 2537 Name: user_id, dtype: int64
print('БЕЗ ЧИСТКИ события: ' , data['date'].count())
print('ПОСЛЕ ЧИСТКИ события: ' , data_2w['date'].count())
print('Потеряли событий: ' , (data['date'].count()- data_2w['date'].count())/data['date'].count()*100)
БЕЗ ЧИСТКИ события: 243713 ПОСЛЕ ЧИСТКИ события: 240887 Потеряли событий: 1.159560630741897
print('БЕЗ ЧИСТКИ пользователи: ' , data['user_id'].nunique())
print('ПОСЛЕ ЧИСТКИ пользователи: ' , data_2w['user_id'].nunique())
print('Потеряли пользователей: ' , (data['user_id'].nunique()-data_2w['user_id'].nunique()) / data['user_id'].nunique()*100)
БЕЗ ЧИСТКИ пользователи: 7551 ПОСЛЕ ЧИСТКИ пользователи: 7534 Потеряли пользователей: 0.22513574361011784
# сравниваем группы 246-247 и 247-248
group_user = data_2w.groupby('group').agg({'user_id': 'nunique'})
group_user['lost_user'] = group_user.user_id/ group_user.user_id.shift(1)
group_user.reset_index()
| group | user_id | lost_user | |
|---|---|---|---|
| 0 | 246 | 2484 | NaN |
| 1 | 247 | 2513 | 1.011675 |
| 2 | 248 | 2537 | 1.009550 |
Посчитала долю от предыдущей группы между 246 -247 и 247-248.Тем самым подтвердив, что размер группы не сильно отличаются друг от друга.
Таким образом, во вторую неделю попадет почти 99% лога и 7534 из 7551 пользователей. Среди них представители всех трех групп эксперимента и размеры групп примерно равны.
# посмотрим количество событий в логах
data_user_count = data_2w.groupby('event').agg({'user_id': 'count'}).sort_values(by='user_id', ascending=False).reset_index()
data_user_count
| event | user_id | |
|---|---|---|
| 0 | MainScreenAppear | 117328 |
| 1 | OffersScreenAppear | 46333 |
| 2 | CartScreenAppear | 42303 |
| 3 | PaymentScreenSuccessful | 33918 |
| 4 | Tutorial | 1005 |
# посчитаем долю пользователей, которые хоть раз совершали событие.
funnel = data_2w.groupby('event').agg({'event': 'count','user_id': 'nunique'}).sort_values(by='user_id', ascending=False)
funnel.set_axis(['count_event','uniq_users'], axis='columns', inplace=True)
funnel['share_event'] = round(funnel.uniq_users / data_2w['user_id'].nunique()*100,2)
funnel.reset_index()
| event | count_event | uniq_users | share_event | |
|---|---|---|---|---|
| 0 | MainScreenAppear | 117328 | 7419 | 98.47 |
| 1 | OffersScreenAppear | 46333 | 4593 | 60.96 |
| 2 | CartScreenAppear | 42303 | 3734 | 49.56 |
| 3 | PaymentScreenSuccessful | 33918 | 3539 | 46.97 |
| 4 | Tutorial | 1005 | 840 | 11.15 |
Всего 5 событий: MainScreenAppear, OffersScreenAppear, CartScreenAppear, PaymentScreenSuccessful, Tutorial.
Порядок происхождения событий.
Первые четыре события образуют последовательную цепочку движения пользователя по приложению. Обучение же не является обязательным этапом.
Не будем учитывать Tutorial - руководство(просмотр обучающей информации для новых пользователей) при расчете воронки(11,15%).
# убираем событие Tutorial
data_4event_name= data_2w.query('event != "Tutorial"')
data_4event_name['user_id'].nunique()
7530
# сгруппируем таблицу по событиям
event_users = data_4event_name.groupby('event').agg({'user_id':'nunique'}).reset_index()
event_users.set_axis(['event', 'users_count'], axis='columns', inplace=True)
# добавитим столбец - какая доля уникальных пользователей совершала это событие
event_users['share_user'] = round(event_users.users_count / data_4event_name['user_id'].nunique()*100)
event_users = event_users.sort_values('users_count', ascending=False).reset_index(drop=True)
# построим воронку с помощью shift
event_users['funnel'] = round(event_users.users_count / event_users.users_count.shift(1)*100,)
event_users
| event | users_count | share_user | funnel | |
|---|---|---|---|---|
| 0 | MainScreenAppear | 7419 | 99.0 | NaN |
| 1 | OffersScreenAppear | 4593 | 61.0 | 62.0 |
| 2 | CartScreenAppear | 3734 | 50.0 | 81.0 |
| 3 | PaymentScreenSuccessful | 3539 | 47.0 | 95.0 |
# сгруппируем таблицу по событиям
event_users = data_4event_name.groupby('event').agg({'user_id':'nunique'}).reset_index()
event_users.set_axis(['event', 'users_count'], axis='columns', inplace=True)
# добавитим столбец - какая доля уникальных пользователей совершала это событие
event_users['share_user'] = round(event_users.users_count / data_4event_name['user_id'].nunique()*100)
event_users = event_users.sort_values('users_count', ascending=False).reset_index(drop=True)
# построим воронку
event_users['funnel'] = 1
for i in range(1, 4):
event_users.loc[i, 'funnel'] = event_users.loc[i, 'users_count'] / (event_users.loc[i-1, 'users_count'])
event_users['funnel'] = round(event_users['funnel']*100)
event_users
| event | users_count | share_user | funnel | |
|---|---|---|---|---|
| 0 | MainScreenAppear | 7419 | 99.0 | 100.0 |
| 1 | OffersScreenAppear | 4593 | 61.0 | 62.0 |
| 2 | CartScreenAppear | 3734 | 50.0 | 81.0 |
| 3 | PaymentScreenSuccessful | 3539 | 47.0 | 95.0 |
Для последовательности событий A → B → C посчитайте отношение числа пользователей с событием B к количеству пользователей с событием A, а также отношение числа пользователей с событием C к количеству пользователей с событием B.
# строим воронку
fig = go.Figure(go.Funnel(y = event_users['event'],
x = event_users['users_count'],
opacity = 0.6,
textposition = 'inside',
textinfo = 'value + percent previous'))
fig.update_layout(title_text='Воронка событий', title_x = 0.5)
fig.show()
Даже первый шаг воронки проходит не 100 % пользователей - получается, на главную страницу заходить не обязательно. Самый большой скачок посещаемости - между главной страницей и страницей предложений (каталогом) - почти 38 % пользователей туда не приходят. Зато на следующих шагах отсеивается всего 19 % и 5 % пользователей соответветственно. Всего до успешной оплаты доходят 47% пользователей приложения.
# посчитаем количество пользователей по группам
users = data_4event_name.groupby('group')['user_id'].nunique()
users
group 246 2483 247 2512 248 2535 Name: user_id, dtype: int64
Всего имеются три группы пользователей: группы 246 и 247 - контрольные, группа 248 - экспериментальная. В них соответственно по 2483, 2512 и 2535 человек.
Мы знаем, что у нас есть 2 контрольные группы для А/А-эксперимента (246 и 247), чтобы проверить корректность всех механизмов и расчётов, и одна тестовая группа (В, 248).
Критерии успешного A/A теста:
print(f'Разница между группой 246 и 247 составляет {data_4event_name.query("group == 247")["user_id"].nunique() / data_4event_name.query("group == 246")["user_id"].nunique():.2}', '%')
Разница между группой 246 и 247 составляет 1.0 %
Количество пользователей в каждой из групп достаточно большое для проведения исследования и разница между размерами групп 246 и 247 незначительная.
# распределение пользователей по группам
len(data_4event_name.groupby('user_id')['group'].nunique().reset_index().query('group > 1'))
0
Среди пользователей нет попавших в несколько групп.
Остается проверить различие ключевых метрик. Для каждого события подсчитаем, какая доля пользователей в каждой группе его совершила, и проверим, является ли отличие между группами статистически достоверным.
# формируем события по группам
event_group = (data_4event_name.
groupby(['event', 'group']).
agg({'user_id': 'nunique'}).
reset_index().
rename(columns={'user_id' : 'total_users'}).
sort_values(by=['group','total_users'], ascending=False))
event_group
| event | group | total_users | |
|---|---|---|---|
| 5 | MainScreenAppear | 248 | 2493 |
| 8 | OffersScreenAppear | 248 | 1531 |
| 2 | CartScreenAppear | 248 | 1230 |
| 11 | PaymentScreenSuccessful | 248 | 1181 |
| 4 | MainScreenAppear | 247 | 2476 |
| 7 | OffersScreenAppear | 247 | 1520 |
| 1 | CartScreenAppear | 247 | 1238 |
| 10 | PaymentScreenSuccessful | 247 | 1158 |
| 3 | MainScreenAppear | 246 | 2450 |
| 6 | OffersScreenAppear | 246 | 1542 |
| 0 | CartScreenAppear | 246 | 1266 |
| 9 | PaymentScreenSuccessful | 246 | 1200 |
#строим воронку событий в разрезе тестовых групп
fig = go.Figure()
fig.add_trace(go.Funnel(name = '246',
y = event_group.query('group == 246')['event'],
x = event_group.query('group == 246')['total_users'],
opacity = 0.7,
textposition = 'inside',
textinfo = 'value + percent previous'))
fig.add_trace(go.Funnel(name = '247',
y = event_group.query('group == 247')['event'],
x = event_group.query('group == 247')['total_users'],
opacity = 0.7,
textposition = 'inside',
textinfo = 'value + percent previous'))
fig.add_trace(go.Funnel(name = '248',
y = event_group.query('group == 248')['event'],
x = event_group.query('group == 248')['total_users'],
opacity = 0.7,
textposition = 'inside',
textinfo = 'value + percent previous'))
fig.update_layout(title_text='Воронка событий по группам' , title_x = 0.5)
fig.show()
В А/В-тестировании проверяем гипотезу о равенстве выборок.
Но сначала проведем (А/А-тест) и проверим находят ли статистические критерии разницу между выборками 246 и 247.
Будем использовать z-критерий. Это статистический тест, который позволяет определить различия между двумя средними значениями генеральной совокупности(когда дисперсии известны и размер выборки велик).
Напишем функцию:
Функция принимает на вход два датафрейма с логами и по заданному событию попарно проверяет есть ли статистически значимая разница между долями пользователей, совершивших его в группе 1 и группе 2.
Входные параметры:
def z_test(trial1, trial2, event, alpha, n):
# критический уровень статистической значимости c поправкой Бонферрони
bonferroni_alpha = alpha / n
# число пользователей в группе 1 и группе 2:
users_new = np.array([trial1['user_id'].nunique(),
trial2['user_id'].nunique()])
# число пользователей, совершивших событие в группе 1 и группе 2
success = np.array([trial1[trial1['event'] == event]['user_id'].nunique(),
trial2[trial2['event'] == event]['user_id'].nunique()])
# пропорции успехов в группах:
p1 = success[0]/users_new[0]
p2 = success[1]/users_new[1]
# пропорция успехов в комбинированном датасете:
p_combined = (success[0] + success[1]) / (users_new[0] + users_new[1])
# разница пропорций в датасетах
difference = p1 - p2
# считаем статистику в ст.отклонениях стандартного нормального распределения
z_value = difference / np.sqrt(p_combined * (1 - p_combined) * (1/users_new[0] + 1/users_new[1]))
# задаем стандартное нормальное распределение (среднее 0, ст.отклонение 1)
distr = st.norm(0, 1)
p_value = (1 - distr.cdf(abs(z_value))) * 2 #тест двусторонний, удваиваем результат
print('Событие:', event)
print('p-значение: ', p_value)
if p_value < bonferroni_alpha:
print('Отвергаем нулевую гипотезу: между долями есть разница')
else:
print('Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными')
Сопоставим доли по каждому событию(контрольными и экспериментальнмы группами):
В нашем исследовании 4 вида событий:
MainScreenAppear, OffersScreenAppear, CartScreenAppear, PaymentScreenSuccessful.
Значит будет проведено 4 A/A теста и 12 А/В. Чтобы застроховать себя от ложного резульата вводим поправку Бонферрони bonferroni_alpha = alpha / 4 для A/A теста и bonferroni_alpha = alpha / 12 для А/В.
Формулируем гипотизу:
Гипотеза H0: между долями нет значимой разницы
Гипотеза H1: между долями есть значимая разница
data_4event_name[data_4event_name['group'] == 246]
| event | user_id | event_time | group | date | |
|---|---|---|---|---|---|
| 2827 | MainScreenAppear | 3737462046622621720 | 2019-08-01 00:08:00 | 246 | 2019-08-01 |
| 2828 | MainScreenAppear | 3737462046622621720 | 2019-08-01 00:08:55 | 246 | 2019-08-01 |
| 2829 | OffersScreenAppear | 3737462046622621720 | 2019-08-01 00:08:58 | 246 | 2019-08-01 |
| 2832 | OffersScreenAppear | 3737462046622621720 | 2019-08-01 00:10:26 | 246 | 2019-08-01 |
| 2833 | MainScreenAppear | 3737462046622621720 | 2019-08-01 00:10:47 | 246 | 2019-08-01 |
| ... | ... | ... | ... | ... | ... |
| 243707 | MainScreenAppear | 5746969938801999050 | 2019-08-07 21:12:11 | 246 | 2019-08-07 |
| 243709 | MainScreenAppear | 5849806612437486590 | 2019-08-07 21:13:59 | 246 | 2019-08-07 |
| 243710 | MainScreenAppear | 5746969938801999050 | 2019-08-07 21:14:43 | 246 | 2019-08-07 |
| 243711 | MainScreenAppear | 5746969938801999050 | 2019-08-07 21:14:58 | 246 | 2019-08-07 |
| 243712 | OffersScreenAppear | 5746969938801999050 | 2019-08-07 21:15:17 | 246 | 2019-08-07 |
78985 rows × 5 columns
# считаем статистическую значимость между контрольными группами 246 и 247:
for event in event_group['event'].unique():
z_test(data_4event_name[data_4event_name['group'] == 246], data_4event_name[data_4event_name['group'] == 247], event, 0.05, 4)
print()
Событие: MainScreenAppear p-значение: 0.7526703436483038 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными Событие: OffersScreenAppear p-значение: 0.24786096925282264 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными Событие: CartScreenAppear p-значение: 0.22867643757335676 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными Событие: PaymentScreenSuccessful p-значение: 0.11446627829276612 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными
По резульату А/А теста между группами 246 и 247 ни по одному событию нет статистически достоверного отличия при заданном уровне значимости. Самое min значение у события PaymentScreenSuccessful - 0.114.
Запускаем А/В тест.
# считаем статистическую значимость между контрольной и экспериментальной группами 246 и 248:
for event in event_group['event'].unique():
z_test(data_4event_name[data_4event_name['group'] == 246], data_4event_name[data_4event_name['group'] == 248], event, 0.05, 12)
print()
Событие: MainScreenAppear p-значение: 0.3387114076159288 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными Событие: OffersScreenAppear p-значение: 0.21442476639710506 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными Событие: CartScreenAppear p-значение: 0.08067367598823139 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными Событие: PaymentScreenSuccessful p-значение: 0.21693033984516674 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными
По резульату А/B теста между группами 246 и 248 ни по одному событию нет статистически достоверного отличия при заданном уровне значимости. Самое min значение у события CartScreenAppear - 0.081.
# считаем статистическую значимость между контрольной и экспериментальной группами 247 и 248:
for event in event_group['event'].unique():
z_test(data_4event_name[data_4event_name['group'] == 247], data_4event_name[data_4event_name['group'] == 248], event, 0.05, 12)
print()
Событие: MainScreenAppear p-значение: 0.5194964354051703 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными Событие: OffersScreenAppear p-значение: 0.9333751305879443 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными Событие: CartScreenAppear p-значение: 0.5878284605111943 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными Событие: PaymentScreenSuccessful p-значение: 0.7275718682261119 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными
По резульату А/B теста между группами 247 и 248 ни по одному событию нет статистически достоверного отличия при заданном уровне значимости. Самое min значение у события MainScreenAppear - 0.519.
# считаем статистическую значимость между объединённой контрольной 246+247 и экпериментальной 248 группами:
for event in event_group['event'].unique():
z_test(data_4event_name[data_4event_name['group'] != 248], data_4event_name[data_4event_name['group'] == 248], event, 0.05,12)
print()
Событие: MainScreenAppear p-значение: 0.3486684291093256 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными Событие: OffersScreenAppear p-значение: 0.44582745409482394 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными Событие: CartScreenAppear p-значение: 0.18683558686831558 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными Событие: PaymentScreenSuccessful p-значение: 0.6107918742187335 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными
По резульату А/B теста между группами 246+247 и 248 ни по одному событию нет статистически достоверного отличия при заданном уровне значимости. Самое min значение у события CartScreenAppear - 0.187.
Так как в нашем эксперименте 4 пары групп и 4 этапа в воронке, число гипотез равно 16, и используя поправку Бонферрони, значение p-value следовало бы уменьшить до 0,003(0.05/16).
ВЫВОДЫ:
В ходе исследования мы изучили данные о работе мобильного приложения-магазина. В анализе участвовали четыре события, образующие последовательные цепочки движения пользователя по приложению: MainScreenAppear-7419, OffersScreenAppear - 4593, CartScreenAppear - 3734, PaymentScreenSuccessful - 3539. Tutorial было исключено из анализа ввиду необязательного прохождения и отсутствия влияния на остальные шаги.
Были проанализированы поведение покупателей на основании логов пользователей, а так же, результаты А/А/В-теста. Тест был запущен 1-го августа, а предыдущие события - это старые логи, которые "доехали" в выборку.Даже первый шаг воронки проходит не 100 % пользователей - получается, на главную страницу заходить не обязательно. Самый большой скачок посещаемости - между главной страницей и страницей предложений (каталогом) - почти 38 % пользователей туда не приходят. Возможно, следует поискать проблему в этом этапе. Зато на следующих шагах отсеивается всего 19 % и 5 % пользователей соответветственно. Всего до успешной оплаты доходят 47% пользователей приложения.
В анализе участвовало:2483 пользователя - 246 группы; 2512 пользователя - 247 группы; 2535 пользователя - 248 группы, где 246-247-это контрольные группы, а 248 - экспериментальная.
Сопоставили доли по каждому событию(контрольными и экспериментальнмы группами):
Всего проведено 16 тестов: 4 A/A теста и 12 А/В.
За время проведения A/A/B-эксперимента по каждому из событий не обнаружили статистически значимой разницы между группами. Из этого можно сделать вывод, что изменение шрифтов во всём приложении на поведение пользователей не влияет. Можно поиграть с цветом(шрифта), формой и размером приложения. Проанализировать, почему теряется большое количество пользователей при переходе : на страницы предложений и корзины.
# посчитаем количество пользователей по группам
users = data_4event_name.pivot_table(index = 'group', values = 'user_id', aggfunc = 'nunique')
users
| user_id | |
|---|---|
| group | |
| 246 | 2483 |
| 247 | 2512 |
| 248 | 2535 |
# Сделам таблицу с числом уникальных пользователей по событиям и контрольным группам
pivot_log = data_4event_name.pivot_table(index='group',columns = 'event', values='user_id',aggfunc='nunique').sort_values(by = 'group', ascending = True)
pivot_log['total'] = [users['user_id'].iloc[0], users['user_id'].iloc[1], users['user_id'].iloc[2]]
pivot_log
| event | CartScreenAppear | MainScreenAppear | OffersScreenAppear | PaymentScreenSuccessful | total |
|---|---|---|---|---|---|
| group | |||||
| 246 | 1266 | 2450 | 1542 | 1200 | 2483 |
| 247 | 1238 | 2476 | 1520 | 1158 | 2512 |
| 248 | 1230 | 2493 | 1531 | 1181 | 2535 |
# добавляем столбец 246+247
new_row = pivot_log.loc[246] + pivot_log.loc[247]
new_row.name = '246_247'
pivot_log = pivot_log.append([new_row])
pivot_log
C:\Users\Tima\AppData\Local\Temp\ipykernel_7008\3657894602.py:4: FutureWarning: The frame.append method is deprecated and will be removed from pandas in a future version. Use pandas.concat instead.
| event | CartScreenAppear | MainScreenAppear | OffersScreenAppear | PaymentScreenSuccessful | total |
|---|---|---|---|---|---|
| group | |||||
| 246 | 1266 | 2450 | 1542 | 1200 | 2483 |
| 247 | 1238 | 2476 | 1520 | 1158 | 2512 |
| 248 | 1230 | 2493 | 1531 | 1181 | 2535 |
| 246_247 | 2504 | 4926 | 3062 | 2358 | 4995 |
# создаем функцию
def z_test(part0, part1, total0, total1, alpha,n):
# доля успехов в первой группе:
p1 = part0 / total0
# доля успехов во второй группе:
p2 = part1 / total1
# комбинированная доля успехов:
p_combined = (part0 + part1) / (total0 + total1)
#разница в долях между группами:
difference = p1 - p2
# считаем статистику в ст.отклонениях стандартного нормального распределения:
z_value = difference / mth.sqrt(p_combined * (1 - p_combined) * (1/total0 + 1/total1))
#задаем стандарное нормальное отклонение(среднее 0, ст.отклонение 1):
distr = st.norm(0, 1)
p_value = (1 - distr.cdf(abs(z_value))) * 2 #тест двусторонний, удваиваем результат
bonferroni_alpha = alpha / n
print('Проверка для групп {} и {} событие: {}, p-значение: {p_value:.2f}'.format(part0, part1, event,p_value=p_value))
if p_value < bonferroni_alpha:
print("Отвергаем нулевую гипотезу: между долями есть разница")
else:
print("Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными")
А/А-тест
# считаем статистическую значимость между контрольными группами 246 и 247:
group_1 = 246
current_row = pivot_log.loc[group_1]
group_2 = 247
for event_number in range(4):
event = pivot_log.columns[event_number]
p_value_result = z_test(current_row[event], pivot_log[event][group_2], current_row['total'], pivot_log['total'][group_2], 0.05,4)
Проверка для групп 1266 и 1238 событие: CartScreenAppear, p-значение: 0.23 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными Проверка для групп 2450 и 2476 событие: MainScreenAppear, p-значение: 0.75 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными Проверка для групп 1542 и 1520 событие: OffersScreenAppear, p-значение: 0.25 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными Проверка для групп 1200 и 1158 событие: PaymentScreenSuccessful, p-значение: 0.11 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными
А/B-тесты
# считаем статистическую значимость между контрольными группами 246 и 248:
group_1 = 246
current_row = pivot_log.loc[group_1]
group_2 = 248
for event_number in range(4):
event = pivot_log.columns[event_number]
p_value_result = z_test(current_row[event], pivot_log[event][group_2], current_row['total'], pivot_log['total'][group_2], 0.05, 12)
Проверка для групп 1266 и 1230 событие: CartScreenAppear, p-значение: 0.08 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными Проверка для групп 2450 и 2493 событие: MainScreenAppear, p-значение: 0.34 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными Проверка для групп 1542 и 1531 событие: OffersScreenAppear, p-значение: 0.21 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными Проверка для групп 1200 и 1181 событие: PaymentScreenSuccessful, p-значение: 0.22 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными
# считаем статистическую значимость между контрольными группами 247 и 248:
group_1 = 247
current_row = pivot_log.loc[group_1]
group_2 = 248
for event_number in range(4):
event = pivot_log.columns[event_number]
p_value_result = z_test(current_row[event], pivot_log[event][group_2], current_row['total'], pivot_log['total'][group_2], 0.05, 12)
Проверка для групп 1238 и 1230 событие: CartScreenAppear, p-значение: 0.59 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными Проверка для групп 2476 и 2493 событие: MainScreenAppear, p-значение: 0.52 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными Проверка для групп 1520 и 1531 событие: OffersScreenAppear, p-значение: 0.93 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными Проверка для групп 1158 и 1181 событие: PaymentScreenSuccessful, p-значение: 0.73 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными
# считаем статистическую значимость между контрольными группами 246+247 и 248:
group_1 = '246_247'
current_row = pivot_log.loc[group_1]
group_2 = 248
for event_number in range(4):
event = pivot_log.columns[event_number]
p_value_result = z_test(current_row[event], pivot_log[event][group_2], current_row['total'], pivot_log['total'][group_2], 0.05, 12)
Проверка для групп 2504 и 1230 событие: CartScreenAppear, p-значение: 0.19 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными Проверка для групп 4926 и 2493 событие: MainScreenAppear, p-значение: 0.35 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными Проверка для групп 3062 и 1531 событие: OffersScreenAppear, p-значение: 0.45 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными Проверка для групп 2358 и 1181 событие: PaymentScreenSuccessful, p-значение: 0.61 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными
При множественном тесте с каждой новой проверкой гипотезы растёт вероятность ошибки первого рода.Вероятность того, что хотя бы в одном из 16 сравнений будет зафиксирован ложнопозитивный результат равна: 1(1-0.05)16 = 56%.В случае четырёх групп вероятность хотя бы одного ложнопозитивного результата уже примерно 18.55%.